﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

namespace PARCS.Core
{
    /// <summary>
    /// Points can connect with chanells and run inside themselves algorythmic modules. Point object in AM is implementation of
    /// remote point, and is used to creating channel with it (CreateChannel()), and to running AM inside it (Execute(string)).
    /// Point can be created in AM with Task.CreatePoint() or with constuctor Point(Task).
    /// </summary>
    public class Point
    {
        /// <summary>
        /// Host, with wich point is connected.
        /// </summary>
        public HostInfo Host { get; private set; }

        /// <summary>
        /// Channel to this point if it's remote point; or to the parent point, if it's local.
        /// </summary>
        public Channel Channel { get; private set; }

        /// <summary>
        /// Channels for connecting with other points
        /// </summary>
        public Task CurrentTask { get; private set; }

        /// <summary>
        /// Number of the point. Setting by the host server.
        /// </summary>
        public int Number { get; set; }

        /// <summary>
        /// Number of the parent point.
        /// </summary>
        public int ParentNumber { get; set; }

        /// <summary>
        /// Creates point at the daemon side with existing data.
        /// </summary>
        /// <param name="socket">Socket to connect</param>
        /// <param name="currentTask">Channels to connecting with other points</param>
        /// <param name="number">Number of the point</param>
        public Point(Socket socket, Task currentTask, int number)
        {
            Host = new HostInfo(((IPEndPoint)socket.RemoteEndPoint).Address)
                       {
                           Socket = socket
                       };
            CurrentTask = currentTask;
            Number = number;
        }

        /// <summary>
        /// Creates new point 
        /// </summary>
        /// <param name="askLocation">If true - ask host server to register</param>
        /// <param name="currentTask">Channels to connecting with other points</param>
        /// <param name="parentNumber">Number of the point</param>
        public Point(bool askLocation, Task currentTask, int parentNumber)
        {
            if (!askLocation) return;

            CurrentTask = currentTask;
            var server = currentTask.GetServer();
            ParentNumber = parentNumber;

            const int ipLength = server.ipAddress.getAddress().length;

            var query = new ServerQuery();

            byte[] ip;

            do
            {
                var outputStream = query.GetOutputStream(5);

                try
                {
                    outputStream.writeByte(Const.HsCreatePoint);
                    outputStream.writeInt(curTask.number);
                    outputStream.writeInt(parentNumber);
                }
                catch (IOException e)
                {
                    Console.Out.WriteLine(e.Message);
                }

                if (query.Send(server, ipLength + 4) == null) return;

                var inputStream = query.GetInputStream();
                ip = new byte[ipLength];

                try
                {
                    Number = inputStream.ReadInt();
                    inputStream.Read(ip);
                }
                catch (IOException e)
                {
                    Console.Out.WriteLine(e.Message);
                }
            }
            while (Number == Const.HsWait);

            try
            {
                Host = new HostInfo(IPAddress.Parse(Encoding.UTF8.GetString(ip)));
            }
            catch (Exception e)
            {
                Console.Out.WriteLine("Host which address received from server doesn't exist");
            }
        }

        /// <summary>
        /// Creates point with asking host server to  register. Equals to Point(true, currentTask, parentNumber)
        /// </summary>
        /// <param name="currentTask"></param>
        /// <param name="parentNumber"></param>
        public Point(Task currentTask, int parentNumber)
            : this(true, currentTask, parentNumber)
        {
        }

        /// <summary>
        /// Removes the point and sends message to the server
        /// </summary>
        public void Remove()
        {
            var server = CurrentTask.GetServer();

            var query = new ServerQuery();
            var outputStream = query.GetOutputStream(9);
            try
            {
                outputStream.writeByte(Const.HsRemovePoint);
                outputStream.writeInt(CurrentTask.Number);
                outputStream.writeInt(Number);
            }
            catch (IOException e)
            {
                Console.Out.WriteLine(e.Message);
            }

            query.Send(server, 0);
        }

        /// <summary>
        /// Creates a channel with this point by connecting to daemon at the current host
        /// </summary>
        /// <returns>If can't connect - null</returns>
        public Channel CreateChannel()
        {
            if (!Host.IsConnected())
            {
                if (!Host.CycleConnect())
                {
                    Console.Out.WriteLine("createChannel: Cannot connect to daemon on " + Host);
                    return null;
                }
            }


            try
            {
                var inputStream = new BinaryReader(new NetworkStream(Host.Socket));
                var outputStream = new BinaryWriter(new NetworkStream(Host.Socket));

                Channel = new Channel(outputStream, inputStream, Number, -1, CurrentTask.Recovery)
                              {
                                  IsWorking = Host.IsConnected()
                              };

                if (CurrentTask.Recovery != null)
                    CurrentTask.Recovery.CreateChildEvent(ParentNumber, Number);
            }
            catch (IOException e)
            {
                Console.Out.WriteLine("initChannel: " + e.Message);
                return null;
            }

            try
            {
                Channel.Write(Const.DmReceiveTask);
                Channel.Write(CurrentTask);
                Channel.Write(Number);

                List<string> fileNames = CurrentTask.GetFileNames();

                if (fileNames.Count > 0)
                {
                    Channel.Write(Const.DmLoadDll);
                    Channel.Write(fileNames.Count);

                    fileNames.ForEach(x => SendFile(x));
                }
                else
                {
                    Channel.Flush();
                }
            }
            catch (IOException e)
            {
                Console.Out.WriteLine("initChannel: " + e.Message);
                return null;
            }

            //TODO:ClassLoader
            return Channel;
        }

        /// <summary>
        /// Executes at this point AM
        /// </summary>
        /// <param name="algorythmicModuleName">AM's name</param>
        public void Execute(string algorythmicModuleName)
        {
            if (!Channel.IsWorking)
            {
                Console.Out.WriteLine("Error: channel broken");
                return;
            }

            ExecuteClass(algorythmicModuleName);
        }

        public void Execute(string[] classes)
        {
            if (!Channel.IsWorking)
            {
                Console.Out.WriteLine("Error: channel broken");
                return;
            }

            if (classes.Length == 0)
            {
                Console.Out.WriteLine("Error: empty array of classes");
                return;
            }

            try
            {
                Channel.Write(Const.DmLoadClasses);
                Channel.Write(classes.Length);
            }
            catch (IOException e)
            {
                Console.Out.WriteLine("execute: " + e);
                return;
            }

            foreach (var s in classes)
            {
                SendFile(s.Replace('.', '/') + ".dll");
            }

            ExecuteClass(classes[0]);
        }

        private bool ExecuteClass(string name)
        {
            try
            {
                Channel.Write(Const.DmExecuteClass);
                Channel.Write(name);
            }
            catch (IOException e)
            {
                Console.Out.WriteLine("executeClass: " + e);
                return false;
            }
            return true;
        }

        /// <summary>
        /// Sends file to the stream
        /// </summary>
        /// <param name="fullName">Full name of the file (with directories)</param>
        /// <returns>True - if sent</returns>
        private bool SendFile(string fullName)
        {
            try
            {
                var fileInfo = new FileInfo(fullName);

                var size = fileInfo.Length;

                var name = fileInfo.Name;

                Channel.Write(name);

                if (!Channel.ReadBool()) return true;

                Channel.Write(size);

                var buffer = new byte[size < 8192 ? size : 8192];

                Channel.SendFile(fullName);

                Console.Out.WriteLine("File " + fullName + " was sent to host " + host);
                return true;
            }
            catch (IOException e)
            {
                Console.Out.WriteLine(e.StackTrace);
                return false;
            }

        }

        /// <summary>
        /// Creates chanel with point
        /// </summary>
        /// <param name="point">Point to connect</param>
        /// <returns>True - if created</returns>
        public bool ConnectPoint(Point point)
        {
            return ConnectPoint(null, point, null);
        }

        /// <summary>
        /// Creates channel wit point
        /// </summary>
        /// <param name="name">Name of the channel of current point. If null - ignore</param>
        /// <param name="point">Point to connect</param>
        /// <param name="pointName">Name of the channel of this point. If null - ignore</param>
        /// <returns>True - if channel successfully created </returns>
        public bool ConnectPoint(string name, Point point, string pointName)
        {
            try
            {
                Channel.Write(Const.DmConnectPoint);
                Channel.Write(point.Host.Address.ToString());
                Channel.Write(name);
                Channel.Write((byte)Number);
                Channel.Write(pointName);
                Channel.Write((byte)point.Number);
                return Channel.ReadBool();
            }
            catch (IOException e)
            {
                Console.Out.WriteLine("createChannel: " + e.Message);
                return false;
            }
        }

        /**
         * Добавляет данные к данной точке, которые будут доступны в AM.run
         * @param key ключ
         * @param value значение
         */
        public void AddData(Object key, Object value)
        {
            try
            {
                Channel.Write(Const.DmAddPointData);
                Channel.Write(Number);
                Channel.Write(key);
                Channel.Write(value);
            }
            catch (IOException e)
            {
                Console.Out.WriteLine("createChannel: " + e.Message);
            }
        }

        private static bool CanRead(string file)
        {
            if (string.IsNullOrEmpty(file))
            {
                Console.Out.WriteLine("execute: filename == null");
                return false;
            }

            if (FileCanBeRead(file)) return true;

            if (!file.EndsWith(".dll"))
            {
                if (FileCanBeRead(file + ".dll")) return true;
            }

            Console.Out.WriteLine("Cannot read file " + file);
            return false;
        }

        private static bool FileCanBeRead(string file)
        {
            if (File.Exists(file))
            {
                try
                {
                    File.OpenRead(file);
                }
                catch (Exception)
                {
                    return false;
                }

                return true;
            }

            return false;
        }
    }
}
